




namespace Orz.LogManager
{
	//========================================================================
	// 	Imported Namespaces
	//========================================================================
	using System;
	using System.IO;
	using System.Text;
	using System.Text.RegularExpressions;
	using Orz.LogManager.Config;
	using Routrek.SSHC;
	using Routrek.Crypto;
	using Routrek.PKI;
	using System.Net;
	using System.Net.Sockets;
	using System.Threading;
	using System.Text;
	using System.Diagnostics;
	
	
	
	public enum SshAuthMechanism
	{
		PublicKey,			// Rely on your computer's public key being already installed on the server
		Password			// You'll already have been prompted for a password by our GUI (or stored it insecurely)
	}
	
	
	
	
	///
	/// <summary>
	/// Connects to and tails a remote data source using SSH.
	/// </summary>
	///
	public class SshFileStream : ILogManagerStream
	{
		//================================================================
		// 	Private Data Members
		//================================================================
		
		/// <summary>The default port for SSH connections</summary>
		private const int SSH_DEFAULT_PORT = 22;
		
		/// <summary>The main ssh connection object</summary>
		private SSHConnection connection_i;
		
		private SSHChannel channel_i;
		
		/// <summary>User name for logging on</summary>
		private string sUser_i;
		
		/// <summary>Password for logging on</summary>
		private string sPassword_i;
		
		/// <summary>Whether to use a password for logging on</summary>
		private SshAuthMechanism authMechanism_i;
		
		/// <summary>Hostname that we're connecting to - not used at the minute</summary>
		private string sHost_i;
				
		/// <summary>The auto-login commands and expected responses</summary>
		private logsetLogfileAutoLogin[] autoLoginDetails_i;
		
		/// <summary>The command to execute once the 'auto login' has completed</summary>
		private string sTelnetCommand_i;
		
		/// <summary>The path of the logfile on the remote system</summary>
		private string sPath_i;
		
		/// <summary><code>true</code> if the 'auto login' is done and the main command has been executed</summary>
		private bool bExecuting_i = false;
		
		/// <summary>The main telnet connection object</summary>
		private StringBuilder buffer_i = new StringBuilder();
		
		/// <summary>The next auto-login command to execute</summary>
		private int iNextAutoLoginIndex_i = 0;
		
		/// <summary>The pattern of the next auto-login command</summary>
		private bool bWroteFirstAutoLoginPrompt_i = false;
		
		/// <summary>Unix request to kill the current remote tail process</summary>
		private const char CTRL_C = (char)3;
		
		/// <summary>Command to print the entire file in unix</summary>
		private const string CAT_COMMAND = "cat ";				
		
		/// <summary>Holds text received since the last time we sent an autologin cmd</summary>
		private string sBufferSinceLastAutoLogin_i = "";
		
		/// <summary>String representation of this stream, based on host etc.</summary>
		private string sDebugIdentifier_i;
		
		/// <summary>Keeps track of the number of connections that have been opened</summary>
		private static int iConnectionCounter_c = 1;
				
		private Reader reader;
		
		//================================================================
		// 	Constructors
		//================================================================

		///
		/// <summary>
		/// Stores the input parameters and creates a new telnet connection.
		/// The sPassword_p parameter can be null/blank unless you're using the 'Password'
		/// authentication mechanism.
		/// </summary>
		///
		public SshFileStream (string sHost_p, int iPort_p, string sUser_p, SshAuthMechanism authMechanism_p, string sPassword_p, string sTelnetCommand_p, string sPath_p, logsetLogfileAutoLogin[] autoLoginDetails_p)
		{			
			sTelnetCommand_i = sTelnetCommand_p;
			sUser_i = sUser_p;
			authMechanism_i = authMechanism_p;
			sPassword_i = sPassword_p;
			sPath_i = sPath_p;
			autoLoginDetails_i = autoLoginDetails_p;
			
			sDebugIdentifier_i = CreateDebugIdentifier(sHost_p);
			
			LogDebug("Connecting to " + sHost_p + " with user " + sUser_p + " and path " + sPath_p);
			
			connection_i = Connect(sUser_p, iPort_p, authMechanism_p, sPassword_p, sHost_p);
		}
		
		
		
		
	
		//================================================================
		// 	Methods
		//================================================================
		
		///
		/// <summary>
		/// Closes the telnet connection.
		/// </summary>
		///
		public void Close ()
		{
			LogDebug("Closing connection");
			connection_i.Close();
			connection_i = null;
		}
		
		
		
		
		///
		/// <summary>
		/// Flushes the output buffer and returns its contents.
		/// </summary>
		///
		public string ReadToEnd ()
		{
			StringBuilder bufferCopy = buffer_i;
			buffer_i = new StringBuilder();
			
			return bufferCopy.ToString();
		}
		
		
		
		///
		/// <summary>
		/// Return to the start of the file.
		/// </summary>
		/// <remarks>
		/// Sends a Ctrl+C to the tail process, followed by a 'cat' of the file,
		/// and then starts the 'tail' up again.
		/// </remarks>		
		///
		public void SeekToStart()
		{			
			LogDebug("Seeking to start - will lose " + buffer_i.Length + " chars");
			
			buffer_i = new StringBuilder();			
			SendData (CTRL_C.ToString());
			
			// If we don't pause here then the Ctrl+C won't be processed
			// before the "cat" command is sent.
			System.Threading.Thread.Sleep(2000);
			
			SendData (CAT_COMMAND + sPath_i + "\n");
			SendData (sTelnetCommand_i + sPath_i + "\n");
		}
		
		
		
		///
		/// <summary>
		/// Clears the contents of the buffer.
		/// </summary>
		///
		public void SeekToEnd()
		{
			LogDebug("Seeking to end - will lose " + buffer_i.Length + " chars");
			buffer_i = new StringBuilder();
		}
		
		
		
		private void SendData (string sData_p)
		{			
			channel_i.Transmit(Encoding.ASCII.GetBytes(sData_p));
		}
		
		
			
		///
		/// <summary>
		/// Handles text being returned from the telnet connection.
		/// </summary>
		/// <remarks>
		/// This method is called whenever text is sent from the remote connection.
		/// If there are still auto-login commands to be processed, these will
		/// be sent; otherwise, if the main tail command has not yet been run, it will be run
		/// now.  If the tail command has already been started, the text will
		/// be appended to the output buffer.
		/// </remarks>		
		///
		public void OnTextReceived (string sText_p)
		{
			LogDebug("Received " + sText_p.Length + " bytes");
			
			try
			{						
				if (! bExecuting_i)
				{
					if (iNextAutoLoginIndex_i < autoLoginDetails_i.Length)
					{					
						sBufferSinceLastAutoLogin_i += sText_p;
						
						// Still more autologin commands to execute
						LogDebug("Still another " + (autoLoginDetails_i.Length - iNextAutoLoginIndex_i) + " autologin events to process..");
						
						logsetLogfileAutoLogin autoLogin = autoLoginDetails_i[iNextAutoLoginIndex_i];
						
						if (0 == iNextAutoLoginIndex_i  &&  !bWroteFirstAutoLoginPrompt_i)
						{
							LogDebug("This is the first autologin prompt - waiting for pattern <" + autoLogin.pattern + ">");
							
							buffer_i.Append ("Waiting for AutoLogin prompt: " + autoLogin.pattern + "...");	
							bWroteFirstAutoLoginPrompt_i = true;
						}
											
						Regex oRegex = new Regex (autoLogin.pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
						
						if (oRegex.IsMatch(sBufferSinceLastAutoLogin_i))
						{
							LogDebug("Found a match for autologin string <" + autoLogin.pattern + ">");
							
							// This is one of the lines we've been expecting -
							// send the response string
							iNextAutoLoginIndex_i++;
							
							if (autoLogin.send.encrypted)
							{
								SendData (PasswordHelper.Decrypt(autoLogin.send.Value) + "\n");
							}
							else
							{
								SendData (autoLogin.send.Value + "\n");
							}
							
							// Inform the user that another autologin prompt has been found.
							buffer_i.Append ("done\n");
							
							if (iNextAutoLoginIndex_i < autoLoginDetails_i.Length)
							{
								// Print the next autologin pattern (if there is one)
								autoLogin = autoLoginDetails_i[iNextAutoLoginIndex_i];
								buffer_i.Append ("Waiting for AutoLogin prompt: " + autoLogin.pattern + "...");
							}		
							
							// Reset the buffer we're using to keep track of all data since the last
							// time we recognised an autologin parameter.
							sBufferSinceLastAutoLogin_i = "";
						}
						else
						{
							LogDebug("No match for autologin pattern <" + autoLogin.pattern + "> in text of length " + sBufferSinceLastAutoLogin_i.Length);						
						}
					}
					else
					{					
						// Run the tail command
						LogDebug("Sending command: <" + sTelnetCommand_i + sPath_i + ">");
						
						SendData (sTelnetCommand_i + sPath_i + "\n");
						bExecuting_i = true;
					}				
				}
				else
				{							
					buffer_i.Append (sText_p);
				}
			}
			catch (Exception e)
			{
				System.Windows.Forms.MessageBox.Show ("exceptionff " + e);
			}
		}
		
		
		
		
		private SSHConnection Connect(string sUser_p, int iPort_p, SshAuthMechanism authMechanism_p, string sPassword_p, string sHost_p)
		{
			if (null == sPassword_p)
			{
				sPassword_p = "";
			}
			
			SSHConnectionParameter param = new SSHConnectionParameter();
			param.UserName = sUser_p;
			param.Password = sPassword_p;			
			param.Protocol = SSHProtocol.SSH2;			
			param.WindowSize = 0x1000; // TODO					
			
			switch (authMechanism_p)
			{
				case SshAuthMechanism.Password:
					param.AuthenticationType = AuthenticationType.Password;
					break;
					
				case SshAuthMechanism.PublicKey:
					param.AuthenticationType = AuthenticationType.PublicKey;
					break;
					
				default:
					throw new ArgumentException("Unknown value for auth mechanism: " + authMechanism_p);					
			}
			

			reader = new Reader();
			Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
			
			if (0 == iPort_p)
			{
				sock.Connect(getEndPoint (sHost_p, SSH_DEFAULT_PORT));
			}
			else
			{
				sock.Connect(getEndPoint (sHost_p, iPort_p));
			}
			
			SSHConnection connection = SSHConnection.Connect(param, reader, sock);
			
			reader.Connection = connection;
			this.channel_i = connection.OpenShell(reader);			 
			reader.Channel = channel_i;
			reader.Parent = this;
			//Debug.WriteLine(connection_i.ConnectionInfo.DumpHostKeyInKnownHostsStyle());
			SSHConnectionInfo connectionInfo = connection.ConnectionInfo;
			
			//Thread.Sleep(1000);
			//((SSH2Connection)connection_i).ReexchangeKeys();
			
			return connection;
		}
		
		///
		/// <summary>
		/// Converts the given host:port settings to an IP address.
		/// </summary>
		///
		private IPEndPoint getEndPoint (string sHost_p, int iPort_p)
		{			
			IPHostEntry hostEntry = System.Net.Dns.GetHostByName(sHost_p);
			return new IPEndPoint(hostEntry.AddressList[0], iPort_p);
		}
		
		
		
		private void LogDebug(string sText_p)
		{
			Log.Debug(sDebugIdentifier_i + sText_p);
		}
		
		
		private string CreateDebugIdentifier(string sHost_p)
		{
			return "(SSH: " + sHost_p + " #" + iConnectionCounter_c++ + ") ";			
		}

	}
	
	
	
	
	
	
	
	
	class Reader : ISSHConnectionEventReceiver, 
	               ISSHChannelEventReceiver
	{
		public SSHConnection Connection;
		public bool IsReady;
		public SSHChannel Channel;
		public SshFileStream Parent;
		private static int iConnectionCounter_c = 1;
		private string sDebugIdentifier_i = "(SSH Reader #" + iConnectionCounter_c++ + ") ";
		
		public void OnData(byte[] data, int offset, int length) 
		{
			string sData = Encoding.ASCII.GetString(data, offset, length);			
			//System.Console.Write(sData);
			this.Parent.OnTextReceived(sData);
		}
		public void OnDebugMessage(bool always_display, byte[] data) {
			LogDebug("DebugMessage: "+ Encoding.ASCII.GetString(data));
		}
		public void OnIgnoreMessage(byte[] data) {
			LogDebug("Ignore: "+ Encoding.ASCII.GetString(data));
		}
		public void OnAuthenticationPrompt(string[] msg) {
			LogDebug("Auth Prompt "+msg[0]);
		}

		public void OnError(Exception error, string msg) {
			LogDebug("ERROR: "+ msg);
		}
		public void OnChannelClosed() {
			LogDebug("Channel closed");
			this.Connection.Disconnect("");
			//connection_i.AsyncReceive(this);
		}
		public void OnChannelEOF() {
			this.Channel.Close();
			LogDebug("Channel EOF");
		}
		public void OnExtendedData(int type, byte[] data) {
			LogDebug("EXTENDED DATA");
		}
		public void OnConnectionClosed() {
			LogDebug("Connection closed");
		}
		public void OnUnknownMessage(byte type, byte[] data) {
			LogDebug("Unknown Message " + type);
		}
		public void OnChannelReady() {
			this.IsReady = true;
		}
		public void OnChannelError(Exception error, string msg) {
			LogDebug("Channel ERROR: "+ msg);
		}
		public void OnMiscPacket(byte type, byte[] data, int offset, int length) {
		}

		public PortForwardingCheckResult CheckPortForwardingRequest(string host, int port, string originator_host, int originator_port) {
			PortForwardingCheckResult r = new PortForwardingCheckResult();
			r.allowed = true;
			r.channel = this;
			return r;
		}
		public void EstablishPortforwarding(ISSHChannelEventReceiver rec, SSHChannel channel) 
		{
			this.Channel = channel;
		}
		
		
		private void LogDebug(string sText_p)
		{
			Log.Debug(sDebugIdentifier_i + sText_p);
		}
		

	}
}
